﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
using System.IO;

namespace MvcCms.Service.Code.Membership
{
    public static class PasswordResetToken
    {

        /// <summary>
        /// Helper function used to generate a token to be used in the message sent to users when registered the first time to confirm their email address.
        /// </summary>
        /// <param name="email">The email address to encode.</param>
        /// <returns>The token generated from the email address, timestamp, and SEED value.</returns>
        public static string GetTokenForValidation(string email, string seedPassword)
        {
            if (String.IsNullOrEmpty(email))
                throw new ArgumentException("The email cannot be null");

            string token = EncodeMessageWithPassword(String.Format("{0}#{1}", email, DateTime.Now), seedPassword);

            return token;
        }

        /// <summary>
        /// Gets the email from token.
        /// </summary>
        /// <param name="token">The token.</param>
        /// <param name="email">The email.</param>
        /// <returns></returns>
        public static bool GetEmailFromToken(string token, string seedPassword, out string email)
        {
            email = String.Empty;


            string message = DecodeMessageWithPassword(token, seedPassword);
            string[] messageParts = message.Split('#');

            if (messageParts.Count() != 2)
            {
                return false;
                // the token was not generated correctly.
            }
            else
            {
                email = messageParts[0];
                return true;
            }
        }

        /// <summary>
        /// Core method to validate a token that also offers a timestamp for testing.  In production mode should always be DateTime.Now.
        /// </summary>
        /// <param name="token">The token to validate.</param>
        /// <param name="email">the email address to use in the validation.</param>
        /// <param name="timestamp">The timestamp representing the time in which the validation is performed.</param>
        /// <returns><c>true</c> if the token is valid, <c>false</c> otherwise.</returns>
        public static bool IsTokenValid(string token, string email, string seedPassword, DateTime timestamp)
        {
            if (String.IsNullOrEmpty(token))
                throw new ArgumentException("The token cannot be null");

            try
            {
                string message = DecodeMessageWithPassword(token, seedPassword);
                string[] messageParts = message.Split('#');

                if (messageParts.Count() != 2)
                {
                    return false;
                    // the token was not generated correctly.
                }
                else
                {
                    string messageEmail = messageParts[0];
                    string messageDate = messageParts[1];

                    // If the emails are the same and the date in which the token was created is no longer than 5 days, then it is valid. Otherwise, it is not. 
                    return (String.Compare(email, messageEmail, true) == 0 && timestamp.Subtract(DateTime.Parse(messageDate)).Days < 5);
                }
            }
            catch (Exception)
            {
                // could not decrypt the message. The token has been tampered with.
                return false;
            }
        }

        /// <summary>
        /// Takes a string and generates a hash value of 16 bytes.
        /// </summary>
        /// <param name="str">The string to be hashed</param>
        /// <param name="passwordFormat">Selects the hashing algorithm used. Accepted values are "sha1" and "md5".</param>
        /// <returns>A hex string of the hashed password.</returns>
        public static string EncodeString(string str, string passwordFormat)
        {
            if (str == null)
                return null;

            ASCIIEncoding AE = new ASCIIEncoding();
            byte[] result;
            switch (passwordFormat)
            {
                case "sha1":
                    SHA1 sha1 = new System.Security.Cryptography.SHA1CryptoServiceProvider();
                    result = sha1.ComputeHash(AE.GetBytes(str));
                    break;
                case "md5":
                    MD5 md5 = new System.Security.Cryptography.MD5CryptoServiceProvider();
                    result = md5.ComputeHash(AE.GetBytes(str));
                    break;
                default:
                    throw new ArgumentException("Invalid format value. Accepted values are 'sha1' and 'md5'.", "passwordFormat");
            }

            // Loop through each byte of the hashed data 
            // and format each one as a hexadecimal string.
            StringBuilder sb = new StringBuilder(16);
            for (int i = 0; i < result.Length; i++)
            {
                sb.Append(result[i].ToString("x2"));
            }


            return sb.ToString();
        }

        ///// <summary>
        ///// Takes a string and generates a hash value of 16 bytes.  Uses "md5" by default.
        ///// </summary>
        ///// <param name="str">The string to be hashed</param>
        ///// <returns>A hex string of the hashed password.</returns>
        //public static string EncodeString(string str)
        //{
        //    return EncodeString(str, "md5");
        //}

        /// <summary>
        /// Takes a string and generates a hash value of 16 bytes.
        /// </summary>
        /// <param name="str">The string to be hashed</param>
        /// <param name="passwordFormat">Selects the hashing algorithm used. Accepted values are "sha1" and "md5".</param>
        /// <returns>A string of the hashed password.</returns>
        public static string EncodeBinary(byte[] buffer, string passwordFormat)
        {
            if (buffer == null)
                return null;

            byte[] result;
            switch (passwordFormat)
            {
                case "sha1":
                    SHA1 sha1 = new System.Security.Cryptography.SHA1CryptoServiceProvider();
                    result = sha1.ComputeHash(buffer);
                    break;
                case "md5":
                    MD5 md5 = new System.Security.Cryptography.MD5CryptoServiceProvider();
                    result = md5.ComputeHash(buffer);
                    break;
                default:
                    throw new ArgumentException("Invalid format value. Accepted values are 'sha1' and 'md5'.", "passwordFormat");
            }


            // Loop through each byte of the hashed data 
            // and format each one as a hexadecimal string.
            StringBuilder sb = new StringBuilder(16);
            for (int i = 0; i < result.Length; i++)
            {
                sb.Append(result[i].ToString("x2"));
            }


            return sb.ToString();
        }

        ///// <summary>
        ///// Encodes the buffer using the default cryptographic provider.
        ///// </summary>
        ///// <param name="buffer">The buffer.</param>
        ///// <returns></returns>
        //public static string EncodeBinary(byte[] buffer)
        //{
        //    return EncodeBinary(buffer, "md5");
        //}

        /// <summary>
        /// Takes a text message and encrypts it using a password as a key.
        /// </summary>
        /// <param name="plainMessage">A text to encrypt.</param>
        /// <param name="password">The password to encrypt the message with.</param>
        /// <returns>Encrypted string.</returns>
        /// <remarks>This method uses TripleDES symmmectric encryption.</remarks>
        public static string EncodeMessageWithPassword(string plainMessage, string password)
        {
            if (plainMessage == null)
                throw new ArgumentNullException("encryptedMessage", "The message cannot be null");

            TripleDESCryptoServiceProvider des = new TripleDESCryptoServiceProvider();
            des.IV = new byte[8];

            //Creates the key based on the password and stores it in a byte array.
            PasswordDeriveBytes pdb = new PasswordDeriveBytes(password, new byte[0]);
            des.Key = pdb.CryptDeriveKey("RC2", "MD5", 128, new byte[8]);

            MemoryStream ms = new MemoryStream(plainMessage.Length * 2);
            CryptoStream encStream = new CryptoStream(ms, des.CreateEncryptor(), CryptoStreamMode.Write);
            byte[] plainBytes = Encoding.UTF8.GetBytes(plainMessage);
            encStream.Write(plainBytes, 0, plainBytes.Length);
            encStream.FlushFinalBlock();
            byte[] encryptedBytes = new byte[ms.Length];
            ms.Position = 0;
            ms.Read(encryptedBytes, 0, (int)ms.Length);
            encStream.Close();

            return Convert.ToBase64String(encryptedBytes);
        }

        /// <summary>
        /// Takes an encrypted message using TripleDES and a password as a key and converts it to the original text message.
        /// </summary>
        /// <param name="encryptedMessage">The encrypted message to decode.</param>
        /// <param name="password">The password to decode the message.</param>
        /// <returns>The Decrypted message</returns>
        /// <remarks>This method uses TripleDES symmmectric encryption.</remarks>
        public static string DecodeMessageWithPassword(string encryptedMessage, string password)
        {
            if (encryptedMessage == null)
                throw new ArgumentNullException("encryptedMessage", "The encrypted message cannot be null");

            TripleDESCryptoServiceProvider des = new TripleDESCryptoServiceProvider();
            des.IV = new byte[8];

            //Creates the key based on the password and stores it in a byte array.
            PasswordDeriveBytes pdb = new PasswordDeriveBytes(password, new byte[0]);
            des.Key = pdb.CryptDeriveKey("RC2", "MD5", 128, new byte[8]);

            //This line protects the + signs that get replaced by spaces when the parameter is not urlencoded when sent.
            encryptedMessage = encryptedMessage.Replace(" ", "+");
            MemoryStream ms = new MemoryStream(encryptedMessage.Length * 2);
            CryptoStream decStream = new CryptoStream(ms, des.CreateDecryptor(), CryptoStreamMode.Write);

            byte[] plainBytes;
            try
            {
                byte[] encBytes = Convert.FromBase64String(Convert.ToString(encryptedMessage));
                decStream.Write(encBytes, 0, encBytes.Length);
                decStream.FlushFinalBlock();
                plainBytes = new byte[ms.Length];
                ms.Position = 0;
                ms.Read(plainBytes, 0, (int)ms.Length);
                decStream.Close();
            }
            catch (CryptographicException e)
            {
                throw new ApplicationException("Cannot decrypt message.  Possibly, the password is wrong", e);
            }

            return Encoding.UTF8.GetString(plainBytes);
        }
    }
}

